iT邦幫忙

2026 iThome 鐵人賽

DAY 23
0
Build on Google AI

從 Vibe Coding 到 Production:用 Google AI 打造上線守門員系列 第 23

Day 23|讓另一個 Agent 挑錯:用對抗驗證淘汰假警報

  • 分享至 

  • xImage
  •  

Day 22,我們把 Vibe Guard 拆成 Recon、Context、Hunter、Validator 與 Reporter。

其中有一條重要限制:

Hunter 只能提出 Candidate。
只有 Validator 可以輸出 Confirmed Finding。

不過,只把角色改名還不夠。

如果 Validator:

  • 沿用 Hunter 選定的 Context。
  • 只執行 Hunter 建議的測試。
  • 閱讀 Hunter 完整推理後再表示同意。
  • 使用相同的漏洞假設搜尋程式碼。

它仍然可能只是替 Hunter 補證據,而不是真正挑錯。

今天要加入一個立場更明確的 Challenger Agent

它收到 Candidate 後,不是問:

我要如何證明這個漏洞?

而是問:

這個 Finding 可能錯在哪裡?
哪個前提尚未成立?
是否存在其他控制?
能否用執行結果推翻它?

我們會準備三個看起來都是 High 的 Candidate:

AUTH-01   所有登入者都能存取他人的 Project
SECRET-01 Client 中的 Firebase API Key 洩漏正式資料庫權限
RATE-01   缺少 Rate Limit Middleware,可以造成正式服務 DoS

Challenger 會使用獨立 Context 與五項測試重新判斷。

最後只有一項能存活。

對抗驗證不是 Prompt Injection 測試

「對抗」在這裡不是讓另一個 Agent 攻擊模型,也不是把惡意字串塞進 Prompt。

它指的是兩個角色具有相反目標:

Hunter
嘗試建立可信的攻擊假設。

Challenger
嘗試找到足以推翻該假設的反證。

如果 Challenger 找不到反證,而且動態測試能重現邊界跨越與影響,Candidate 才能升級。

Cloudflare security-audit-skill 的目前公開流程,也要求每個 Candidate 交給 Fresh Verifier 嘗試駁回。

它把結果分成:

confirmed
needs_validation
rejected

Vibe Guard 目前使用的對應名稱為:

confirmed
requires_evidence
rejected

名稱略有不同,核心原則相同:

無法確認,不等於確認安全;無法推翻,也不等於已確認漏洞。

為什麼「請仔細檢查」不夠?

假設 Hunter 回傳:

High:API Key 被寫在 src/main.js,攻擊者可存取正式 Firestore。

如果下一個 Agent 收到的 Prompt 是:

請驗證上述 High 漏洞並補充證據。

任務本身已經預設它是 High 漏洞。

模型很可能繼續尋找:

API Key 在哪一行?
如何旋轉 Key?
如何改成 Environment Variable?

卻沒有先問:

這是 Firebase Client Configuration 嗎?
這個 Key 用來識別 Project,還是授權 Backend Resource?
程式連的是 Emulator 還是 Production?
是否存在非 Localhost 的執行路徑?

因此 Challenger 的 System Instruction 應明確要求:

你的成功條件不是確認 Finding。
你的成功條件是找出足以拒絕、降級或精確限制 Finding 的證據。
不得沿用 Hunter 的 Severity。
不得把缺少證據解讀為最危險情況。

不過,Prompt 只是第一層。

真正的獨立性還需要 Context、Retrieval、工具與 Runtime 的隔離。

獨立驗證至少有六個層次

第一是角色獨立。

找到 Candidate 的 Agent,不能驗證自己的 Candidate。

第二是 Context 獨立。

Challenger 不應只看到 Hunter 選出的支持證據。它要重新取得:

相關設定
替代實作
測試
部署資訊
可能的 Mitigation
Repository 外的 Unknown

第三是 Retrieval 獨立。

Hunter 可能搜尋:

apiKey
rate limit
allow read

Challenger 應該改用反證導向的問題:

這個 Key 實際呼叫哪些 Service?
是否連到 Emulator?
是否有 Runtime Guard?
正式流量在哪一層終止?
是否存在 Owner Check 的安全版本?

第四是工具獨立。

Hunter 可以使用唯讀 Code Search。

Challenger 需要受限的 Test Runner、Emulator、Configuration Inspector 或 Runtime Harness。

第五是模型獨立。

如果成本與環境允許,可以讓 Hunter 與 Challenger 使用不同 Model,降低兩者共享相同盲點的機率。

Cloudflare 的 Vulnerability Harness 也描述了使用不同模型分別執行 Discovery 與 Validation,讓後者以不同權重與訓練資料重新判斷。

但不同 Model 不是充分條件。兩個 Model 若拿到完全相同、已偏向原結論的 Context,仍可能一起犯錯。

第六是執行證據獨立。

Challenger 應該自己執行測試,不接受:

Hunter 表示已成功重現。

它要保存:

使用哪個 Rules File?
建立哪些身分?
執行哪些 Operation?
實際結果是 Allowed 還是 Denied?
是否修改了受害者資料?

Challenger 要執行哪五項測試?

Day 16 曾整理 Cloudflare Validation 的五個方向。

今天將它們真正放進 Demo:

測試 要推翻或確認什麼?
Exploitation 攻擊輸入是否真的能到達並跨越邊界?
Impact 攻擊者實際取得或改變了什麼?
Baseline 系統設計是否本來就允許這種行為?
Mitigation 其他控制是否已經阻擋攻擊?
Runtime Parser、Framework、Rules 或 SDK 的真實行為是什麼?

每項測試只能輸出:

supports
contradicts
unknown

supports 表示結果支持 Candidate。

contradicts 表示找到反證。

unknown 表示目前 Artifact 無法回答,不能自行選擇最危險的假設。

Verdict 不應使用多數決

假設五項測試得到:

3 supports
1 contradicts
1 unknown

不能直接使用:

支持票較多,因此 Confirmed。

安全 Finding 不是投票。

不同測試的重要性不同。

例如:

Exploitation = contradicts

代表攻擊路徑根本無法重現。即使其他四項看起來可疑,也不能稱為 Confirmed Vulnerability。

Vibe Guard 第一版使用以下規則:

Confirmed
  Exploitation 與 Impact 有實際證據,
  Source Evidence 可核對,
  沒有已確認的 Mitigation 阻擋路徑。

Rejected
  核心前提被來源、設定或執行結果推翻。

Requires Evidence
  假設仍可能成立,
  但部署、權限、Runtime 或 Impact 的關鍵事實未知。

Severity 只屬於 Confirmed Finding。

requires_evidence 不應先保留 Hunter 宣稱的 High,否則 Dashboard 仍會把未確認假設當成高風險事件。

先保存 Hunter 原始主張

對抗驗證不能直接覆寫 Candidate。

Demo 的 Hunter Artifact 包含:

{
  id: "SECRET-01",
  ruleId: "SEC-SECRET-001",
  title: "Exposed Firebase API key grants production database access",
  claimedSeverity: "high",
  hypothesis:
    "The apiKey value in client code is a privileged secret that grants access to production Firestore data.",
  hunterObservations: [
    "src/main.js contains apiKey: demo-api-key.",
    "The value is shipped to the browser."
  ]
}

Challenger 不會修改這份資料。

它會另外產生:

{
  "id": "SECRET-01",
  "verdict": "rejected",
  "severity": null,
  "independentSources": [],
  "tests": [],
  "reason": "...",
  "missingEvidence": []
}

這樣才能在評估時回答:

Hunter 為什麼誤判?
哪個反證推翻它?
哪一類 Candidate 最常被拒絕?
是 Retrieval、分類還是 Runtime 理解錯誤?

如果只留下最終 Finding,無法分析被淘汰的假警報。

實作 Day 23 Demo

專案新增:

demo-app/
└── adversarial-validation-demo/
    ├── scenario.js
    └── output/
        ├── candidates.json
        ├── challenges.json
        └── surviving-findings.json

執行:

cd /media/mickey/777/ithome/demo-app
npm run adversarial-validation:demo

package.json 會先重建 Recon,再啟動 Firestore Emulator:

{
  "scripts": {
    "adversarial-validation:demo": "npm run recon:demo >/dev/null && firebase emulators:exec --only firestore \"node adversarial-validation-demo/scenario.js\""
  }
}

這次沒有沿用 Day 22 的 candidate.json 作為唯一 Context。

Challenger 只把三個 Candidate 當成待推翻的主張,接著為每一項重新選擇來源。

AUTH-01:真實問題應該通過更嚴格的挑戰

第一個 Candidate 主張:

一般登入者可以讀取並修改另一個帳號的 Project。

Challenger 獨立讀取:

firestore.rules
review-target/secure-firestore.rules
src/main.js

並使用兩份不同 Rules 執行同一個雙帳號 Scenario。

目前不安全的 Rules 為:

allow read, write: if request.auth != null;

安全版本加入 Owner Check:

allow read, delete: if request.auth != null
  && resource.data.ownerId == request.auth.uid;

allow update: if request.auth != null
  && resource.data.ownerId == request.auth.uid
  && request.resource.data.ownerId == resource.data.ownerId;

測試矩陣為:

Rules Owner Read Attacker Read Attacker Write
Vulnerable Allowed Allowed Allowed
Owner-checking Allowed Denied Denied

這裡不只證明攻擊成功,也證明修正方向真的阻擋相同 Scenario。

五項結果為:

exploitation SUPPORTS
impact       SUPPORTS
baseline     SUPPORTS
mitigation   SUPPORTS
runtime      SUPPORTS

mitigation SUPPORTS 的意思是:

Mitigation Test 支持 Candidate 的判斷品質。

因為安全版本的 Owner Check 能阻擋攻擊,說明問題確實位於原本缺少 Ownership Enforcement,而不是測試本身無效。

AUTH-01 因此存活並成為:

CONFIRMED / HIGH

SECRET-01:看到 apiKey 不等於看到 Secret

第二個 Candidate 主張:

Client 中的 Firebase API Key 是特權憑證,
攻擊者可以用它存取正式 Firestore。

Challenger 沒有先研究如何隱藏 Key,而是重新分類它的用途。

src/main.js 顯示:

const firebaseConfig = {
  apiKey: "demo-api-key",
  authDomain: "demo-vibe-guard.firebaseapp.com",
  projectId: "demo-vibe-guard"
};

接著找到 Runtime Guard:

if (!isLocalDemo) {
  throw new Error(
    "This intentionally vulnerable demo can only run locally."
  );
}

以及:

connectAuthEmulator(auth, "http://127.0.0.1:9099");
connectFirestoreEmulator(db, "127.0.0.1", 8080);

這些證據共同推翻:

Key 連到正式資料庫並授予特權。

Firebase 官方文件也說明,Firebase Service 使用的 API Key 與典型 Secret 不同。

它用於將 Request 關聯到 Firebase Project;Backend Resource Access 應由 Firebase Security Rules 控制,App Authenticity 則由 App Check 補強。

這不代表所有名為 apiKey 的值都可以公開。

如果同一個公開 Key 的 API Restriction 包含 Generative Language API 或其他可計費、非 Firebase Service,就可能產生不同風險。

但目前 Candidate 的明確主張是:

demo-api-key 可以取得正式 Firestore 權限。

這項主張已被 Repository 中的本機執行邊界與 Emulator 設定推翻。

五項測試全部為:

CONTRADICTS

SECRET-01 最終結果是:

REJECTED

不是把 Severity 從 High 改成 Low。

既然核心漏洞敘述不成立,就不應在漏洞清單中留下看似較小的版本。

RATE-01:缺少證據不能直接駁回,也不能確認

第三個 Candidate 主張:

Repository 沒有 Rate Limit Middleware,
所以攻擊者可以讓正式服務發生 DoS。

Challenger 重新讀取:

recon-demo/architecture.json
package.json
firebase.json

發現目前 Repository 描述的是:

Local-only Firebase web application

沒有 Repository 內的 Express 或 Hono Long-running Server。

firebase.json 只有 Hosting 與 Emulator 設定。

Recon 同時把以下問題列為 Unknown:

Are CDN, WAF, rate limits, quotas, or App Check enabled?

因此 Baseline Test 反駁了:

這裡一定應該存在 Application Middleware。

但 Challenger 也不能證明正式環境一定有 CDN、Quota 或 App Check。

Exploitation 與 Impact 仍然是 Unknown:

沒有正式 Endpoint
沒有 Request Budget
沒有 Load Test
沒有 Availability Threshold
沒有 Cost Impact

所以 RATE-01 不能 Confirmed,也不能完全 Rejected。

它的結果是:

REQUIRES_EVIDENCE

並精確要求:

  1. Production Hosting Architecture。
  2. CDN、Gateway、App Check 與 Quota 設定。
  3. 有明確可用性或成本門檻的 Load Test。

這比:

請人工確認是否安全。

更容易轉成下一個可執行任務。

Demo 執行結果

實際輸出:

ADVERSARIAL FINDING VALIDATION
Candidates from Hunter: 3

AUTH-01 HIGH
  Hunter: An authenticated attacker can read and modify a project owned by another account.
  Challenger: fresh context and independent tests
  Verdict: CONFIRMED
  Reason: The candidate survived independent source review, dynamic exploitation, impact, and mitigation tests.
    exploitation SUPPORTS
    impact       SUPPORTS
    baseline     SUPPORTS
    mitigation   SUPPORTS
    runtime      SUPPORTS

SECRET-01 HIGH
  Hunter: The apiKey value in client code is a privileged secret that grants access to production Firestore data.
  Challenger: fresh context and independent tests
  Verdict: REJECTED
  Reason: The Hunter classified a localhost Firebase client identifier as a privileged production credential.
    exploitation CONTRADICTS
    impact       CONTRADICTS
    baseline     CONTRADICTS
    mitigation   CONTRADICTS
    runtime      CONTRADICTS

RATE-01 HIGH
  Hunter: Because the repository has no application rate-limit middleware, an unauthenticated attacker can exhaust the production service.
  Challenger: fresh context and independent tests
  Verdict: REQUIRES_EVIDENCE
  Reason: Missing application middleware does not prove an exploitable production denial of service when the deployment edge is unknown.
    exploitation UNKNOWN
    impact       UNKNOWN
    baseline     CONTRADICTS
    mitigation   UNKNOWN
    runtime      UNKNOWN

SUMMARY
confirmed: 1
requires_evidence: 1
rejected: 1
survival_rate: 1/3
Artifacts: adversarial-validation-demo/output/

三個 Hunter 宣稱的 High Candidate,只有一個進入最終 Findings。

這裡的 Survival Rate 為:

Confirmed Candidates / All Candidates
= 1 / 3

它不是 Agent 的 Accuracy,也不是完整 Precision。

因為被拒絕與待補證據的 Candidate 還需要 Ground Truth,才能判定 Hunter 是否真的錯誤。

不過,Survival Rate 可以作為 Workflow 的營運訊號:

過高
可能代表 Challenger 太寬鬆,什麼都接受。

過低
可能代表 Hunter 太吵,或 Context Retrieval 品質不足。

Day 27 建立標註資料集後,才會正式計算 Precision、Recall 與 False Positive Rate。

不要讓 Challenger 變成另一個 Hunter

Challenger 最常見的失敗,是在驗證過程又發現其他可疑問題,然後直接建立新 Finding。

例如驗證 SECRET-01 時看到:

ownerEmail 被顯示在畫面上。

這可能值得調查,但不屬於目前 Candidate。

Challenger 應該輸出:

out_of_scope_observation

再交給 Orchestrator 建立新的 Hunt Task。

它不能同時扮演:

原 Finding 的裁判
新 Finding 的作者

Cloudflare 也強調 Validator 不應自行提交它發現的 Finding。這能維持 Candidate 與驗證結果的一對一關係,也避免 Validator 因為想保留新發現而放寬自己的證據門檻。

反證也必須有證據

Challenger 不能只說:

我認為這應該不是問題。

Rejected 結果至少要保存:

被推翻的原始主張
使用的獨立來源
哪項測試產生反證
實際檔案或執行結果
為什麼反證足以推翻核心前提

SECRET-01 的反證鏈為:

Client 中有 apiKey
  → 但 Project ID 為 demo-vibe-guard
  → Authentication 連到 127.0.0.1:9099
  → Firestore 連到 127.0.0.1:8080
  → 非 Localhost 會在初始化前 Throw
  → 沒有到達正式 Firestore 的路徑

Rejected 並不是「沒有找到更多證據」,而是已有證據否定 Candidate 的核心攻擊敘述。

Requires Evidence 也要可結案

如果所有不確定問題都永久留在:

requires_evidence

報告只是把噪音移到另一個清單。

每筆待補證據結果應包含:

缺少哪個精確事實?
誰或哪個系統可以提供?
取得後如何改變 Verdict?
何時到期?
如果一直取不到,是否接受風險?

以 RATE-01 為例:

Owner:
  Platform Team

Required:
  Production edge configuration
  Quota and App Check enforcement
  Load-test result

Decision:
  若沒有任何 Edge Control 且測試能造成門檻以上影響,
  才能重新評估為 Confirmed。

Day 24 的 Findings Schema 會把這些生命週期資訊正式加入結構化輸出。

對抗驗證仍然可能一起犯錯

加入 Challenger 不代表報告自動正確。

Hunter 與 Challenger 仍可能共享:

  • 相同的錯誤架構摘要。
  • 同一個缺少檔案的 Search Index。
  • 相同 Model 的盲點。
  • 錯誤的 Test Fixture。
  • 不符合 Production 的 Emulator 行為。
  • 被污染的 Repository 內容。

因此高風險 Finding 還需要:

Mechanical Schema Validation
Source Line Verification
Independent Record Verification
必要時人工覆核

Google Threat Intelligence 公開的 Agentic Vulnerability Discovery Harness,也強調以程式化 Harness 嚴格編排 Agent、加入 Skeptical Validation,並結合人類領域知識。

OpenAI 對大規模 Code Verification 的分享則指出,部署時的 Reviewer 應優先維持高 Signal-to-noise,因為 False Alarm 會消耗人工驗證成本並降低使用者信任。

對 Vibe Guard 而言,乾淨報告不是少報問題,而是清楚區分:

已證明的漏洞
仍缺證據的假設
已被反證的誤報

第一版還缺少什麼?

今天的 Challenger 使用已知 Candidate 與明確測試,目的是先建立對抗驗證契約。

正式版本還需要:

  • 讓 Challenger 自動建立反證導向的 Retrieval Query。
  • 使用不同 Model 或不同 Inference Configuration 交叉驗證。
  • 為每種 Rule 定義必要 Challenge Test。
  • 將 Runtime Test 放入無網路、有限資源的 Sandbox。
  • 區分 Test Failure、Candidate Rejected 與 Workflow Error。
  • 保存 Tool Input、Output、Exit Code、Duration 與環境版本。
  • 驗證 Test Fixture 本身沒有把答案寫死。
  • 對 Critical Finding 要求第二個 Fresh Verifier。
  • 追蹤各 Hunter 的 Confirmation、Rejection 與 Requires Evidence 比例。
  • 將 Out-of-scope Observation 送回新的 Hunt Queue。

最重要的是避免兩個 Agent 使用同一份壓縮摘要。

如果 Summary 在一開始就漏掉 Runtime Guard,後面的 Hunter 與 Challenger 都可能把 demo-api-key 當成正式 Secret。

Fresh Agent 必須搭配 Fresh Retrieval,才有機會找到前一階段忽略的反證。

今天的結論

對抗驗證不是請另一個 Agent 表示同意,而是建立一個以推翻 Candidate 為成功條件的獨立流程。

一個可用的 Challenger 至少要回答:

  1. 核心攻擊前提能否實際重現?
  2. 攻擊者取得了什麼具體影響?
  3. 系統設計是否本來就允許該行為?
  4. 是否存在其他 Mitigation 阻擋路徑?
  5. Runtime 的真實行為是否符合靜態推論?
  6. 哪些反證來自獨立 Retrieval?
  7. 缺少證據時,下一步要取得哪個精確事實?
  8. 被拒絕的 Candidate 是否保存完整原因?

今天三個 Hunter Candidate 的結果為:

AUTH-01   CONFIRMED
SECRET-01 REJECTED
RATE-01   REQUIRES_EVIDENCE

AUTH-01 通過雙帳號攻擊、影響與安全 Rules 對照測試。

SECRET-01 被 Localhost Guard、Firebase Emulator 與 Key 用途分類推翻。

RATE-01 則因正式部署邊界與負載影響未知,不能保留 Hunter 宣稱的 High Severity。

最終只有一項進入 surviving-findings.json

一份可信的 AI 稽核報告,不是 Candidate 越多越好。

真正有價值的是:每個留下來的 Finding,都曾經遇到一個認真想證明它是錯的 Agent。

明天,我們會把 Finding 從一次性的模型回答,擴充成可追蹤驗證、狀態與修正生命週期的完整 Schema。

參考資料


上一篇
Day 22|單一 Agent 不夠用:設計偵察、檢查與驗證的多 Agent 流程
系列文
從 Vibe Coding 到 Production:用 Google AI 打造上線守門員23
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言